// 包fsys是用于读取
// 允许在磁盘上的文件顶部进行虚拟覆盖。
package fsys

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"io/ioutil"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"time"
)

// OverlayFile是OverlayJSON格式的文本文件的路径。
// 它是-overlay标志的值。
var OverlayFile string

// OverlayJSON是覆盖文件的预期格式。
// 替换贴图从覆盖路径映射到替换路径：
// Go命令将转发所有试图打开的读取
// 每个覆盖路径到其替换路径，或考虑覆盖。
// 如果替换路径为空，则路径不存在。
type OverlayJSON struct {
	Replace map[string]string
}

type node struct {
	actualFilePath string           // 如果目录为空，则为空
	children       map[string]*node // 路径元素→ 文件或目录
}

func (n *node) isDir() bool {
	return n.actualFilePath == "" && n.children != nil
}

func (n *node) isDeleted() bool {
	return n.actualFilePath == "" && n.children == nil
}

// TODO（matloob）：将它们封装在类似io/fs的接口中
var overlay map[string]*node // 路径->文件或目录节点
var cwd string               // 复制base.Cwd（）以避免依赖关系

// 规范化用于在覆盖中查找的路径。
// 重要提示：filepath.Join（cwd，path）并不总是生成
// 如果路径是相对的，则为正确的绝对路径，因为启用
// 生成正确绝对路径的窗口需要
// 系统调用。因此，这应该只在查找路径时使用
// 或规范化覆盖中的路径。
func canonicalize(path string) string {
	if path == "" {
		return ""
	}
	if filepath.IsAbs(path) {
		return filepath.Clean(path)
	}

	if v := filepath.VolumeName(cwd); v != "" && path[0] == filepath.Separator {
		// 在Windows文件路径上。Join（cwd，path）并不总是有效。一般来说
		// Abs需要在Windows上进行系统调用。cmd/go中的其他地方
		// 使用filepath.Join（cwd，path），但cmd/go特别支持Windows
		// 以“\”开头的路径，表示该路径相对于
		// 工作目录的卷。见golang.org/issue/8130。
		return filepath.Join(v, path)
	}

	// 使路径绝对化。
	return filepath.Join(cwd, path)
}

// Init初始化覆盖（如果正在使用）。
func Init(wd string) error {
	if overlay != nil {
		// 已初始化
		return nil
	}

	cwd = wd

	if OverlayFile == "" {
		return nil
	}

	b, err := os.ReadFile(OverlayFile)
	if err != nil {
		return fmt.Errorf("reading overlay file: %v", err)
	}

	var overlayJSON OverlayJSON
	if err := json.Unmarshal(b, &overlayJSON); err != nil {
		return fmt.Errorf("parsing overlay JSON: %v", err)
	}

	return initFromJSON(overlayJSON)
}

func initFromJSON(overlayJSON OverlayJSON) error {
	// 规范化覆盖贴图中的路径。
	// 使用reverseCanonicalized检查碰撞：
	// 不应将两个“发件人”路径规范化为同一路径。
	overlay = make(map[string]*node)
	reverseCanonicalized := make(map[string]string) // 与规范化操作相反，用于检查重复项
	// 从替换映射构建文件和目录节点表。

	// 通过对映射进行排序，从映射的迭代中删除任何潜在的非确定性。
	replaceFrom := make([]string, 0, len(overlayJSON.Replace))
	for k := range overlayJSON.Replace {
		replaceFrom = append(replaceFrom, k)
	}
	sort.Strings(replaceFrom)

	for _, from := range replaceFrom {
		to := overlayJSON.Replace[from]
		// 规范化路径并检查冲突。
		if from == "" {
			return fmt.Errorf("empty string key in overlay file Replace map")
		}
		cfrom := canonicalize(from)
		if to != "" {
			// 不要规范化“”，这意味着删除一个文件，因为这样它会变成“”。
			to = canonicalize(to)
		}
		if otherFrom, seen := reverseCanonicalized[cfrom]; seen {
			return fmt.Errorf(
				"paths %q and %q both canonicalize to %q in overlay file Replace map", otherFrom, from, cfrom)
		}
		reverseCanonicalized[cfrom] = from
		from = cfrom

		// 为覆盖的文件创建节点。
		dir, base := filepath.Dir(from), filepath.Base(from)
		if n, ok := overlay[from]; ok {
			// 覆盖中的所有“源”路径都是文件路径。由于路径
			// 在映射中，它们是唯一的，因此如果节点已经存在，则我们添加了
			// 当我们创建父目录节点时，它将显示在下面。就是
			// 文件及其父目录之一的路径都作为键存在
			// 在替换图中。
			// None
			// 这仅适用于覆盖目录包含任何文件或目录的情况
			// 其中：仅包含已删除文件的占位符目录不包含
			// 计数它们可以安全地被实际文件覆盖。
			for _, f := range n.children {
				if !f.isDeleted() {
					return fmt.Errorf("invalid overlay: path %v is used as both file and directory", from)
				}
			}
		}
		overlay[from] = &node{actualFilePath: to}

		// 将父目录节点添加到覆盖结构。
		childNode := overlay[from]
		for {
			dirNode := overlay[dir]
			if dirNode == nil || dirNode.isDeleted() {
				dirNode = &node{children: make(map[string]*node)}
				overlay[dir] = dirNode
			}
			if childNode.isDeleted() {
				// 仅为已删除的文件创建一个父级：
				// 该目录仅在以下情况下有条件存在
				// 存在任何未删除的子项，因此
				// 我们不创造他们的父母。
				if dirNode.isDir() {
					dirNode.children[base] = childNode
				}
				break
			}
			if !dirNode.isDir() {
				// 此路径已作为文件存在，因此不能是父路径
				// 目录见上面的错误评论。
				return fmt.Errorf("invalid overlay: path %v is used as both file and directory", dir)
			}
			dirNode.children[base] = childNode
			parent := filepath.Dir(dir)
			if parent == dir {
				break // 到达顶端；没有父母
			}
			dir, base = parent, filepath.Base(dir)
			childNode = dirNode
		}
	}

	return nil
}

// 如果路径是磁盘上或中的目录，则IsDir返回true
// 覆盖
func IsDir(path string) (bool, error) {
	path = canonicalize(path)

	if _, ok := parentIsOverlayFile(path); ok {
		return false, nil
	}

	if n, ok := overlay[path]; ok {
		return n.isDir(), nil
	}

	fi, err := os.Stat(path)
	if err != nil {
		return false, err
	}

	return fi.IsDir(), nil
}

// parentIsOverlayFile返回name或其中任何一个
// 其父项是覆盖中的文件，找到的第一个父项，
// 包括名称本身，这是覆盖中的一个文件。
func parentIsOverlayFile(name string) (string, bool) {
	if overlay != nil {
		// 检查名称是否可能不是目录，因为
		// 它或它的父对象之一覆盖了一个文件。
		// 托多（马特卢布）：也许可以保存这个以避免每次都这样做？
		prefix := name
		for {
			node := overlay[prefix]
			if node != nil && !node.isDir() {
				return prefix, true
			}
			parent := filepath.Dir(prefix)
			if parent == prefix {
				break
			}
			prefix = parent
		}
	}

	return "", false
}

// errNotDir用于从ReadDir到IsDirWithGoFiles的通信
// 参数不是目录，因此IsDirWithGoFiles不是
// 返回一个错误。
var errNotDir = errors.New("not a directory")

// readDir读取磁盘上的一个目录，如果该目录不是目录，则返回一个错误errNotDir。
// 不幸的是，如果dir不是目录，则ioutil.ReadDir返回的错误
// 根据操作系统（Linux、Mac、Windows return ENOTDIR；BSD returns EINVAL）的不同而有所不同。
func readDir(dir string) ([]fs.FileInfo, error) {
	fis, err := ioutil.ReadDir(dir)
	if err == nil {
		return fis, nil
	}

	if os.IsNotExist(err) {
		return nil, err
	}
	if dirfi, staterr := os.Stat(dir); staterr == nil && !dirfi.IsDir() {
		return nil, &fs.PathError{Op: "ReadDir", Path: dir, Err: errNotDir}
	}
	return nil, err
}

// ReadDir提供了相应的fs.FileInfo条目片段
// 到目录中的覆盖文件。
func ReadDir(dir string) ([]fs.FileInfo, error) {
	dir = canonicalize(dir)
	if _, ok := parentIsOverlayFile(dir); ok {
		return nil, &fs.PathError{Op: "ReadDir", Path: dir, Err: errNotDir}
	}

	dirNode := overlay[dir]
	if dirNode == nil {
		return readDir(dir)
	}
	if dirNode.isDeleted() {
		return nil, &fs.PathError{Op: "ReadDir", Path: dir, Err: fs.ErrNotExist}
	}
	diskfis, err := readDir(dir)
	if err != nil && !os.IsNotExist(err) && !errors.Is(err, errNotDir) {
		return nil, err
	}

	// 统计覆盖中的文件以生成文件信息的复合列表
	files := make(map[string]fs.FileInfo)
	for _, f := range diskfis {
		files[f.Name()] = f
	}
	for name, to := range dirNode.children {
		switch {
		case to.isDir():
			files[name] = fakeDir(name)
		case to.isDeleted():
			delete(files, name)
		default:
			// 这是一个常规文件。
			f, err := os.Lstat(to.actualFilePath)
			if err != nil {
				files[name] = missingFile(name)
				continue
			} else if f.IsDir() {
				return nil, fmt.Errorf("for overlay of %q to %q: overlay Replace entries can't point to dirctories",
					filepath.Join(dir, name), to.actualFilePath)
			}
			// 为覆盖的文件添加fileinfo，以便
			// 原始文件的名称，但覆盖文件的元数据。
			files[name] = fakeFile{name, f}
		}
	}
	sortedFiles := diskfis[:0]
	for _, f := range files {
		sortedFiles = append(sortedFiles, f)
	}
	sort.Slice(sortedFiles, func(i, j int) bool { return sortedFiles[i].Name() < sortedFiles[j].Name() })
	return sortedFiles, nil
}

// OverlayPath返回文件的覆盖内容的路径
// 文件，如果覆盖删除文件，则为空字符串或路径
// 本身如果文件不在覆盖中，则该文件是一个目录
// 在覆盖中，或者没有覆盖。
// 如果路径与常规文件重叠，则返回true
// 或删除，否则为false。
func OverlayPath(path string) (string, bool) {
	if p, ok := overlay[canonicalize(path)]; ok && !p.isDir() {
		return p.actualFilePath, ok
	}

	return path, false
}

// “打开”在给定路径上打开或覆盖该文件。
func Open(path string) (*os.File, error) {
	return OpenFile(path, os.O_RDONLY, 0)
}

// OpenFile使用标志和perm在给定路径上打开或覆盖该文件。
func OpenFile(path string, flag int, perm os.FileMode) (*os.File, error) {
	cpath := canonicalize(path)
	if node, ok := overlay[cpath]; ok {
		// 在覆盖中打开文件。
		if node.isDir() {
			return nil, &fs.PathError{Op: "OpenFile", Path: path, Err: errors.New("fsys.OpenFile doesn't support opening directories yet")}
		}
		// 无法打开覆盖路径进行写入。
		if perm != os.FileMode(os.O_RDONLY) {
			return nil, &fs.PathError{Op: "OpenFile", Path: path, Err: errors.New("overlaid files can't be opened for write")}
		}
		return os.OpenFile(node.actualFilePath, flag, perm)
	}
	if parent, ok := parentIsOverlayFile(filepath.Dir(cpath)); ok {
		// 该文件在替换映射中被显式删除，
		// 或者隐式地，因为它的一个父目录
		// 被文件替换。
		return nil, &fs.PathError{
			Op:   "Open",
			Path: path,
			Err:  fmt.Errorf("file %s does not exist: parent directory %s is replaced by a file in overlay", path, parent),
		}
	}
	return os.OpenFile(cpath, flag, perm)
}

// IsDirWithGoFiles报告dir是否是包含Go文件的目录
// 要么在磁盘上，要么在覆盖层中。
func IsDirWithGoFiles(dir string) (bool, error) {
	fis, err := ReadDir(dir)
	if os.IsNotExist(err) || errors.Is(err, errNotDir) {
		return false, nil
	}
	if err != nil {
		return false, err
	}

	var firstErr error
	for _, fi := range fis {
		if fi.IsDir() {
			continue
		}

		// TODO（matloob）：这将强制地图中的“发件人”
		// 具有.go后缀，但实际的目标文件
		// 不需要有.go后缀。这样可以吗
		// 编译程序？
		if !strings.HasSuffix(fi.Name(), ".go") {
			continue
		}
		if fi.Mode().IsRegular() {
			return true, nil
		}

		// fi是Lstat的结果，因此它不遵循符号链接。
		// 但是，如果文件是指向常规
		// 文件，所以使用os.Stat跟踪符号链接并检查它。
		actualFilePath, _ := OverlayPath(filepath.Join(dir, fi.Name()))
		fi, err := os.Stat(actualFilePath)
		if err == nil && fi.Mode().IsRegular() {
			return true, nil
		}
		if err != nil && firstErr == nil {
			firstErr = err
		}
	}

	// 在目录中找不到go文件。
	return false, firstErr
}

// walk递归地向下移动路径，调用walkFn。抄袭了一些
// 来自路径/filepath.walk的修改。
func walk(path string, info fs.FileInfo, walkFn filepath.WalkFunc) error {
	if !info.IsDir() {
		return walkFn(path, info, nil)
	}

	fis, readErr := ReadDir(path)
	walkErr := walkFn(path, info, readErr)
	// 如果读取错误！=nil，walk无法进入此目录。
	// walkErr！=nil表示walkFn希望walk跳过此目录或停止行走。
	// 因此，如果readErr和walkErr中的一个不是nil，walk将返回。
	if readErr != nil || walkErr != nil {
		// 调用方的行为由返回值控制，返回值由
		// 通过walkFn。walkFn可以忽略readErr并返回nil。
		// 如果walkFn返回SkipDir，它将由调用方处理。
		// 所以walk应该返回walkFn返回的任何内容。
		return walkErr
	}

	for _, fi := range fis {
		filename := filepath.Join(path, fi.Name())
		if walkErr = walk(filename, fi, walkFn); walkErr != nil {
			if !fi.IsDir() || walkErr != filepath.SkipDir {
				return walkErr
			}
		}
	}
	return nil
}

// Walk遍历根目录下的文件树，为每个文件或文件调用walkFn
// 目录树中的目录，包括根目录。
func Walk(root string, walkFn filepath.WalkFunc) error {
	info, err := Lstat(root)
	if err != nil {
		err = walkFn(root, nil, err)
	} else {
		err = walk(root, info, walkFn)
	}
	if err == filepath.SkipDir {
		return nil
	}
	return err
}

// lstat实现了在覆盖文件系统上运行的os.lstat版本。
func Lstat(path string) (fs.FileInfo, error) {
	return overlayStat(path, os.Lstat, "lstat")
}

// Stat实现了在覆盖文件系统上运行的os.Stat版本。
func Stat(path string) (fs.FileInfo, error) {
	return overlayStat(path, os.Stat, "stat")
}

// overlayStat实现lstat或Stat（取决于传入的是os.lstat还是os.Stat）。
func overlayStat(path string, osStat func(string) (fs.FileInfo, error), opName string) (fs.FileInfo, error) {
	cpath := canonicalize(path)

	if _, ok := parentIsOverlayFile(filepath.Dir(cpath)); ok {
		return nil, &fs.PathError{Op: opName, Path: cpath, Err: fs.ErrNotExist}
	}

	node, ok := overlay[cpath]
	if !ok {
		// 文件或目录未覆盖。
		return osStat(path)
	}

	switch {
	case node.isDeleted():
		return nil, &fs.PathError{Op: "lstat", Path: cpath, Err: fs.ErrNotExist}
	case node.isDir():
		return fakeDir(filepath.Base(path)), nil
	default:
		fi, err := osStat(node.actualFilePath)
		if err != nil {
			return nil, err
		}
		return fakeFile{name: filepath.Base(path), real: fi}, nil
	}
}

// fakeFile为覆盖文件提供了fs.FileInfo实现，
// 因此，该文件具有覆盖文件的名称，但采用所有
// 替换文件的其他特征。
type fakeFile struct {
	name string
	real fs.FileInfo
}

func (f fakeFile) Name() string       { return f.name }
func (f fakeFile) Size() int64        { return f.real.Size() }
func (f fakeFile) Mode() fs.FileMode  { return f.real.Mode() }
func (f fakeFile) ModTime() time.Time { return f.real.ModTime() }
func (f fakeFile) IsDir() bool        { return f.real.IsDir() }
func (f fakeFile) Sys() interface{}   { return f.real.Sys() }

// missingFile为覆盖文件提供fs.FileInfo，其中
// 覆盖中的目标文件不存在。它返回零值
// 对于名称以外的fileInfo方法，请设置为文件名和模式
// 设置为不规则。
type missingFile string

func (f missingFile) Name() string       { return string(f) }
func (f missingFile) Size() int64        { return 0 }
func (f missingFile) Mode() fs.FileMode  { return fs.ModeIrregular }
func (f missingFile) ModTime() time.Time { return time.Unix(0, 0) }
func (f missingFile) IsDir() bool        { return false }
func (f missingFile) Sys() interface{}   { return nil }

// fakeDir为以下目录提供了fs.FileInfo实现：
// 由覆盖的文件隐式创建。中的每个目录
// 覆盖文件的路径被认为存在于覆盖文件系统中。
type fakeDir string

func (f fakeDir) Name() string       { return string(f) }
func (f fakeDir) Size() int64        { return 0 }
func (f fakeDir) Mode() fs.FileMode  { return fs.ModeDir | 0500 }
func (f fakeDir) ModTime() time.Time { return time.Unix(0, 0) }
func (f fakeDir) IsDir() bool        { return true }
func (f fakeDir) Sys() interface{}   { return nil }

// Glob类似于filepath.Glob，但使用覆盖文件系统。
func Glob(pattern string) (matches []string, err error) {
	// 检查图案是否成形良好。
	if _, err := filepath.Match(pattern, ""); err != nil {
		return nil, err
	}
	if !hasMeta(pattern) {
		if _, err = Lstat(pattern); err != nil {
			return nil, nil
		}
		return []string{pattern}, nil
	}

	dir, file := filepath.Split(pattern)
	volumeLen := 0
	if runtime.GOOS == "windows" {
		volumeLen, dir = cleanGlobPathWindows(dir)
	} else {
		dir = cleanGlobPath(dir)
	}

	if !hasMeta(dir[volumeLen:]) {
		return glob(dir, file, nil)
	}

	// 防止无限递归。见第15879期。
	if dir == pattern {
		return nil, filepath.ErrBadPattern
	}

	var m []string
	m, err = Glob(dir)
	if err != nil {
		return
	}
	for _, d := range m {
		matches, err = glob(d, file, matches)
		if err != nil {
			return
		}
	}
	return
}

// cleanGlobPath为全局匹配准备路径。
func cleanGlobPath(path string) string {
	switch path {
	case "":
		return "."
	case string(filepath.Separator):
		// 不要破坏这条路
		return path
	default:
		return path[0 : len(path)-1] // 切掉拖尾分离器
	}
}

func volumeNameLen(path string) int {
	isSlash := func(c uint8) bool {
		return c == '\\' || c == '/'
	}
	if len(path) < 2 {
		return 0
	}
	// 带驱动器号
	c := path[0]
	if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
		return 2
	}
	// 是UNC吗？https:
	if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) &&
		!isSlash(path[2]) && path[2] != '.' {
		// 首先，引导“下一步”不应该是“下一步”。它的服务器名。
		for n := 3; n < l-1; n++ {
			// 其次，下一个“\”不应该重复。
			if isSlash(path[n]) {
				n++
				// 第三，跟随一些人物。它的共享名。
				if !isSlash(path[n]) {
					if path[n] == '.' {
						break
					}
					for ; n < l; n++ {
						if isSlash(path[n]) {
							break
						}
					}
					return n
				}
				break
			}
		}
	}
	return 0
}

// cleanGlobPath windows是cleanGlobPath的windows版本。
func cleanGlobPathWindows(path string) (prefixLen int, cleaned string) {
	vollen := volumeNameLen(path)
	switch {
	case path == "":
		return 0, "."
	case vollen+1 == len(path) && os.IsPathSeparator(path[len(path)-1]): // /，\，C:\和C:/
		// 不要破坏这条路
		return vollen + 1, path
	case vollen == len(path) && len(path) == 2: // C:
		return vollen, path + "." // 将C:转换为C:。
	default:
		if vollen >= len(path) {
			vollen = len(path) - 1
		}
		return vollen, path[0 : len(path)-1] // 切掉拖尾分离器
	}
}

// glob在目录dir中搜索与模式匹配的文件
// 并将它们附加到匹配项中。如果无法创建目录
// 打开后，它将返回现有的匹配项。新的比赛正在进行中
// 按字典顺序添加的。
func glob(dir, pattern string, matches []string) (m []string, e error) {
	m = matches
	fi, err := Stat(dir)
	if err != nil {
		return // 忽略I/O错误
	}
	if !fi.IsDir() {
		return // 忽略I/O错误
	}

	list, err := ReadDir(dir)
	if err != nil {
		return // 忽略I/O错误
	}

	var names []string
	for _, info := range list {
		names = append(names, info.Name())
	}
	sort.Strings(names)

	for _, n := range names {
		matched, err := filepath.Match(pattern, n)
		if err != nil {
			return m, err
		}
		if matched {
			m = append(m, filepath.Join(dir, n))
		}
	}
	return
}

// hasMeta报告路径是否包含任何魔法字符
// 由filepath.Match识别。
func hasMeta(path string) bool {
	magicChars := `*?[`
	if runtime.GOOS != "windows" {
		magicChars = `*?[\`
	}
	return strings.ContainsAny(path, magicChars)
}
